package edu.thu.thss.yxy.sip;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;

import javax.sip.SipException;
import javax.sip.SipStack;
import javax.sip.address.Hop;
import javax.sip.address.Router;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.RouteHeader;
import javax.sip.message.Request;

import edu.thu.thss.yxy.util.Logger;
import gov.nist.javax.sip.stack.DefaultRouter;

/**
 * An implementation of the <tt>Router</tt> interface wrapping around JAIN-SIP
 * RI <tt>DefaultRouter</tt> in order to be able to change the outbound proxy
 * depending on the account which sent the request.
 * 
 * @author Andy Gao
 *
 */
public class ProxyRouter implements Router {
	/**
     * Logger for this class.
     */
    private static final Logger logger = Logger.getLogger(ProxyRouter.class);

    /**
     * The running JAIN-SIP stack.
     */
    private final SipStack stack;

    /**
     * Used to cache the <tt>DefaultRouter</tt>s. One <tt>DefaultRouter</tt> per
     * outbound proxy.
     */
    private final Map<String, Router> routerCache
        = new HashMap<String, Router>();

    /**
     *  our outbound proxy if we have one (remains null if we are not using a proxy).
     */
    private ProxyHop outboundProxy = null;
    
    /**
     * The jain-sip router to use for accounts that do not have a proxy or as a
     * default. Do not use this attribute directly but getDefaultRouter() (lazy
     * initialization)
     */
    private Router defaultRouter = null;

    /**
     * Simple constructor. Ignores the <tt>defaultRoute</tt> parameter.
     *
     * @param stack the currently running stack.
     * @param defaultRoute ignored parameter.
     */
    public ProxyRouter(SipStack stack, String defaultRoute)
    {
        if (stack == null)
            throw new IllegalArgumentException("stack shouldn't be null!");
        this.stack = stack;

        if (defaultRoute != null && defaultRoute.length() > 0) {
            this.outboundProxy = new ProxyHop(defaultRoute);
        }
    }
    
    /**
     * Returns the next hop for this <tt>Request</tt>.
     *
     * @param request <tt>Request</tt> to find the next hop.
     * @return the next hop for the <tt>request</tt>.
     */
    public Hop getNextHop(Request request) throws SipException
    {
        return this.getRouterFor(request).getNextHop(request);
    }

    /**
     * Return  the default address to forward the request to.
     * The list is organized in the following priority.
     *
     * If the outbound proxy has been specified, then it is used
     * to construct the first element of the list.
     *
     * If the requestURI refers directly to a host, the host and port
     * information are extracted from it and made the next hop on the
     * list.
     *
     * @param sipRequest is the sip request to route.
     *
     */
	@SuppressWarnings("unchecked")
	public ListIterator<Hop> getNextHops(Request sipRequest) {
		URI requestURI = sipRequest.getRequestURI();
		if (requestURI == null) {
			throw new IllegalArgumentException("Bad message: Null requestURI");
		}

		LinkedList<Hop> hops = new LinkedList<Hop>();
		if (outboundProxy != null) {
			hops.add(outboundProxy);
		}

		ListIterator<RouteHeader> routes = sipRequest
				.getHeaders(RouteHeader.NAME);
		if (routes != null && routes.hasNext()) {
			while (routes.hasNext()) {
				RouteHeader route = (RouteHeader) routes.next();
				SipURI uri = (SipURI) route.getAddress().getURI();
				int port = uri.getPort();
				port = (port == -1) ? 5060 : port;
				String host = uri.getHost();
				String transport = uri.getTransportParam();
				if (transport == null) {
					transport = "UDP";
				}
				Hop hop = new ProxyHop(host + ':' + port + '/' + transport);
				hops.add(hop);
			}
		} else if (requestURI instanceof SipURI
				&& ((SipURI) requestURI).getMAddrParam() != null) {
			SipURI sipURI = ((SipURI) requestURI);
			String maddr = sipURI.getMAddrParam();
			String transport = sipURI.getTransportParam();
			if (transport == null) {
				transport = "UDP";
			}
			int port = 5060;
			Hop hop = new ProxyHop(maddr, port, transport);
			hops.add(hop);
		} else if (requestURI instanceof SipURI) {
			SipURI sipURI = ((SipURI) requestURI);
			int port = sipURI.getPort();
			if (port == -1) {
				port = 5060;
			}
			String host = sipURI.getHost();
			String transport = sipURI.getTransportParam();
			if (transport == null) {
				transport = "UDP";
			}
			Hop hop = new ProxyHop(host + ":" + port + "/" + transport);
			hops.add(hop);
		} else {
			throw new IllegalArgumentException("Malformed requestURI");
		}
		return (hops.size() == 0) ? null : hops.listIterator();
	}

	public Hop getOutboundProxy() {
		logger.fatal("Hello World!", new Exception());
		return this.outboundProxy;
	}
	
	/**
     * Retrieves a DefaultRouter whose default route is the outbound proxy of
     * the account which sent the <tt>request</tt>, or a default one.
     *
     * @param request the <tt>Request</tt> which to retrieve a <tt>Router</tt>
     *            for.
     * @return a <tt>Router</tt> with the outbound proxy set for this
     *         <tt>request</tt> if needed, or a default router
     */
    private Router getRouterFor(Request request)
    {
    	// P2P case
        if (outboundProxy == null)
            return this.getDefaultRouter();

        // outbound proxy case
        Router router = routerCache.get(outboundProxy);
        if (router == null)
        {
            router = new DefaultRouter(stack, outboundProxy.toString());
            routerCache.put(outboundProxy.toString(), router);
        }
        return router;
    }
	
	/**
     * Returns and create if needed a default router (no outbound proxy)
     *
     * @return a router with no outbound proxy set
     */
    private Router getDefaultRouter()
    {
        if (this.defaultRouter == null)
            this.defaultRouter = new DefaultRouter(stack, null);
        return this.defaultRouter;
    }

}
